Linux内核调试方法

您所在的位置:网站首页 调试linux内核 知乎 Linux内核调试方法

Linux内核调试方法

2024-07-17 20:30| 来源: 网络整理| 查看: 265

目录

一  调试前的准备

二  内核中的bug

三  内核调试配置选项

1  内核配置

2  调试原子操作

四  引发bug并打印信息

1  BUG()和BUG_ON()

2.WARN(x) 和 WARN_ON(x)

3 dump_stack()

五  printk()

1  printk函数的健壮性

2  printk函数脆弱之处

3  LOG等级

4  记录缓冲区

5  syslogd/klogd

6  dmesg

7 注意 

8 内核printk和日志系统的总体结构

9  动态调试

六  内存调试工具

1  MEMWATCH

2  YAMD

3  Electric Fence

七  strace

八  OOPS

1  ksymoops

2  kallsyms

3  Kdump

九  KGDB

1  kgdb的调试原理

2  Kgdb的安装与设置

3  在VMware中搭建调试环境

4  kgdb的一些特点和不足

十  使用SkyEye构建Linux内核调试环境

1  SkyEye的安装和μcLinux内核编译

2  使用SkyEye调试

3  使用SkyEye调试内核的特点和不足

十一  KDB

1  入门

2  初始化并设置环境变量

3  激活 KDB

4  KDB 命令

5  技巧和诀窍

6 结束语

十二  Kprobes

1  安装

2  编写 Kprobes 模块

3  使用 Kprobes 更好地进行调试

内核开发比用户空间开发更难的一个因素就是内核调试艰难。内核错误往往会导致系统宕机,很难保留出错时的现场。调试内核的关键在于你的对内核的深刻理解。 

一  调试前的准备

在调试一个bug之前,我们所要做的准备工作有: 

有一个被确认的bug。包含这个bug的内核版本号,需要分析出这个bug在哪一个版本被引入,这个对于解决问题有极大的帮助。可以采用二分查找法来逐步锁定bug引入版本号。对内核代码理解越深刻越好,同时还需要一点点运气。该bug可以复现。如果能够找到复现规律,那么离找到问题的原因就不远了。最小化系统。把可能产生bug的因素逐一排除掉。 二  内核中的bug

内核中的bug也是多种多样的。它们的产生有无数的原因,同时表象也变化多端。从隐藏在源代码中的错误到展现在目击者面前的bug,其发作往往是一系列连锁反应的事件才可能出发的。虽然内核调试有一定的困难,但是通过你的努力和理解,说不定你会喜欢上这样的挑战。 

三  内核调试配置选项

学习编写驱动程序要构建安装自己的内核(标准主线内核)。最重要的原因之一是:内核开发者已经建立了多项用于调试的功能。但是由于这些功能会造成额外的输出,并导致能下降,因此发行版厂商通常会禁止发行版内核中的调试功能。 

1  内核配置

为了实现内核调试,在内核配置上增加了几项:

Kernel hacking ---> [*] Magic SysRq key [*] Kernel debugging [*] Debug slab memory allocations [*] Spinlock and rw-lock debugging: basic checks [*] Spinlock debugging: sleep-inside-spinlock checking [*] Compile the kernel with debug info Device Drivers ---> Generic Driver Options ---> [*] Driver Core verbose debug messages General setup ---> [*] Configure standard kernel features (for small systems) ---> [*] Load all symbols for debugging/ksymoops

启用选项例如: 

slab layer debugging(slab层调试选项) high-memory debugging(高端内存调试选项) I/O mapping debugging(I/O映射调试选项) spin-lock debugging(自旋锁调试选项) stack-overflow checking(栈溢出检查选项) sleep-inside-spinlock checking(自旋锁内睡眠选项) 2  调试原子操作

从内核2.5开发,为了检查各类由原子操作引发的问题,内核提供了极佳的工具。  内核提供了一个原子操作计数器,它可以配置成,一旦在原子操作过程中,进城进入睡眠或者做了一些可能引起睡眠的操作,就打印警告信息并提供追踪线索。  所以,包括在使用锁的时候调用schedule(),正使用锁的时候以阻塞方式请求分配内存等,各种潜在的bug都能够被探测到。  下面这些选项可以最大限度地利用该特性: 

CONFIG_PREEMPT = y CONFIG_DEBUG_KERNEL = y CONFIG_KLLSYMS = y CONFIG_SPINLOCK_SLEEP = y 四  引发bug并打印信息 1  BUG()和BUG_ON()

一些内核调用可以用来方便标记bug,提供断言并输出信息。最常用的两个是BUG()和BUG_ON()。

定义在中:

#ifndef HAVE_ARCH_BUG #define BUG() do { printk("BUG: failure at %s:%d/%s()! ", __FILE__, __LINE__, __FUNCTION__); panic("BUG!"); /* 引发更严重的错误,不但打印错误消息,而且整个系统业会挂起 */ } while (0) #endif #ifndef HAVE_ARCH_BUG_ON #define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while(0) #endif

当调用这两个宏的时候,它们会引发OOPS,导致栈的回溯和错误消息的打印。  ※ 可以把这两个调用当作断言使用,如:BUG_ON(bad_thing); 

2.WARN(x) 和 WARN_ON(x)

而WARN_ON则是调用dump_stack,打印堆栈信息,不会OOPS

定义在中:

#ifndef __WARN_TAINT #ifndef __ASSEMBLY__ extern void warn_slowpath_fmt(const char *file, const int line, const char *fmt, ...) __attribute__((format(printf, 3, 4))); extern void warn_slowpath_fmt_taint(const char *file, const int line, unsigned taint, const char *fmt, ...) __attribute__((format(printf, 4, 5))); extern void warn_slowpath_null(const char *file, const int line); #define WANT_WARN_ON_SLOWPATH #endif #define __WARN() warn_slowpath_null(__FILE__, __LINE__) #define __WARN_printf(arg...) warn_slowpath_fmt(__FILE__, __LINE__, arg) #define __WARN_printf_taint(taint, arg...) \ warn_slowpath_fmt_taint(__FILE__, __LINE__, taint, arg) #else #define __WARN() __WARN_TAINT(TAINT_WARN) #define __WARN_printf(arg...) do { printk(arg); __WARN(); } while (0) #define __WARN_printf_taint(taint, arg...) \ do { printk(arg); __WARN_TAINT(taint); } while (0) #endif #ifndef WARN_ON #define WARN_ON(condition) ({ \ int __ret_warn_on = !!(condition); \ if (unlikely(__ret_warn_on)) \ __WARN(); \ unlikely(__ret_warn_on); \ }) #endif #ifndef WARN #define WARN(condition, format...) ({ \ int __ret_warn_on = !!(condition); \ if (unlikely(__ret_warn_on)) \ __WARN_printf(format); \ unlikely(__ret_warn_on); \ }) #endif 3 dump_stack()

有些时候,只需要在终端上打印一下栈的回溯信息来帮助你调试。这时可以使用dump_stack()。这个函数只在终端上打印寄存器上下文和函数的跟踪线索。 

if (!debug_check) { printk(KERN_DEBUG “provide some information…/n”); dump_stack(); } 五  printk()

内核提供的格式化打印函数。 

1  printk函数的健壮性

      健壮性是printk最容易被接受的一个特质,几乎在任何地方,任何时候内核都可以调用它(中断上下文、进程上下文、持有锁时、多处理器处理时等)。 

2  printk函数脆弱之处

      在系统启动过程中,终端初始化之前,在某些地方是不能调用的。如果真的需要调试系统启动过程最开始的地方,有以下方法可以使用: 

使用串口调试,将调试信息输出到其他终端设备。使用early_printk(),该函数在系统启动初期就有打印能力。但它只支持部分硬件体系。 3  LOG等级

       printk和printf一个主要的区别就是前者可以指定一个LOG等级。内核根据这个等级来判断是否在终端上打印消息。内核把比指定等级高的所有消息显示在终端。         可以使用下面的方式指定一个LOG级别: 

printk(KERN_CRIT  “Hello, world!\n”); 

注意,第一个参数并不一个真正的参数,因为其中没有用于分隔级别(KERN_CRIT)和格式字符的逗号(,)。KERN_CRIT本身只是一个普通的字符串(事实上,它表示的是字符串 "";表 1 列出了完整的日志级别清单)。作为预处理程序的一部分,C 会自动地使用一个名为 字符串串联 的功能将这两个字符串组合在一起。组合的结果是将日志级别和用户指定的格式字符串包含在一个字符串中。  内核使用这个指定LOG级别与当前终端LOG等级console_loglevel来决定是不是向终端打印。  下面是可使用的LOG等级: 

#define KERN_EMERG "" /* system is unusable */ #define KERN_ALERT "" /* action must be taken immediately */ #define KERN_CRIT "" /* critical conditions */ #define KERN_ERR "" /* error conditions */ #define KERN_WARNING "" /* warning conditions */ #define KERN_NOTICE "" /* normal but significant condition */ #define KERN_INFO "" /* informational */ #define KERN_DEBUG "" /* debug-level messages */ #define KERN_DEFAULT "" /* Use the default kernel loglevel */

注意,如果调用者未将日志级别提供给 printk,那么系统就会使用默认值 KERN_WARNING ""(表示只有KERN_WARNING 级别以上的日志消息会被记录)。由于默认值存在变化,所以在使用时最好指定LOG级别。有LOG级别的一个好处就是我们可以选择性的输出LOG。比如平时我们只需要打印KERN_WARNING级别以上的关键性LOG,但是调试的时候,我们可以选择打印KERN_DEBUG等以上的详细LOG。而这些都不需要我们修改代码,只需要通过命令修改默认日志输出级别: 

mtj@ubuntu :~$ cat /proc/sys/kernel/printk 4 4 1 7 mtj@ubuntu :~$ cat /proc/sys/kernel/printk_delay 0 mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit 5 mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit_burst 10

第一项定义了 printk API 当前使用的日志级别。这些日志级别表示了控制台的日志级别、默认消息日志级别、最小控制台日志级别和默认控制台日志级别。printk_delay 值表示的是 printk 消息之间的延迟毫秒数(用于提高某些场景的可读性)。注意,这里它的值为 0,而它是不可以通过 /proc 设置的。printk_ratelimit 定义了消息之间允许的最小时间间隔(当前定义为每 5 秒内的某个内核消息数)。消息数量是由 printk_ratelimit_burst 定义的(当前定义为 10)。如果您拥有一个非正式内核而又使用有带宽限制的控制台设备(如通过串口), 那么这非常有用。注意,在内核中,速度限制是由调用者控制的,而不是在printk 中实现的。如果一个 printk 用户要求进行速度限制,那么该用户就需要调用printk_ratelimit 函数。 

4  记录缓冲区

  内核消息都被保存在一个LOG_BUF_LEN大小的环形队列中。    关于LOG_BUF_LEN定义: 

#define __LOG_BUF_LEN (1 ( ) KGDB: On generic serial port (8250) (X) KGDB: On ethernet

另外使用eth0网口作为调试端口时,grub.list的配置如下: 

title 2.6.7 kgdb root (hd0,0) kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait [email protected]/,@192.168. 6.13/

其他的过程与使用串口作为连接端口时的设置过程相同。  注意:尽管可以使用以太网口作为kgdb的调试端口,使用串口作为连接端口更加简单易行,kgdb项目组推荐使用串口作为调试端口。 2.4  模块的调试方法  内核可加载模块的调试具有其特殊性。由于内核模块中各段的地址是在模块加载进内核的时候才最终确定的,所以develop机的gdb无法得到各种符号地址信息。所以,使用kgdb调试模块所需要解决的一个问题是,需要通过某种方法获得可加载模块的最终加载地址信息,并把这些信息加入到gdb环境中。  I、在Linux 2.4内核中的内核模块调试方法  在Linux2.4.x内核中,可以使用insmod -m命令输出模块的加载信息,例如: 

[root@lisl tmp]# insmod -m hello.ko >modaddr

查看模块加载信息文件modaddr如下: 

.this 00000060 c88d8000 2**2 .text 00000035 c88d8060 2**2 .rodata 00000069 c88d80a0 2**5 …… .data 00000000 c88d833c 2**2 .bss 00000000 c88d833c 2**2 ……

在这些信息中,我们关心的只有4个段的地址:.text、.rodata、.data、.bss。在development机上将以上地址信息加入到gdb中,这样就可以进行模块功能的测试了。 

(gdb) Add-symbol-file hello.o 0xc88d8060 -s .data 0xc88d80a0 -s .rodata 0xc88d80a0 -s .bss 0x c88d833c

这种方法也存在一定的不足,它不能调试模块初始化的代码,因为此时模块初始化代码已经执行过了。而如果不执行模块的加载又无法获得模块插入地址,更不可能在模块初始化之前设置断点了。对于这种调试要求可以采用以下替代方法。  在target机上用上述方法得到模块加载的地址信息,然后再用rmmod卸载模块。在development机上将得到的模块地址信息导入到gdb环境中,在内核代码的调用初始化代码之前设置断点。这样,在target机上再次插入模块时,代码将在执行模块初始化之前停下来,这样就可以使用gdb命令调试模块初始化代码了。  另外一种调试模块初始化函数的方法是:当插入内核模块时,内核模块机制将调用函数sys_init_module(kernel/modle.c)执行对内核模块的初始化,该函数将调用所插入模块的初始化函数。程序代码片断如下: 

…… …… if (mod->init != NULL) ret = mod->init(); …… ……

在该语句上设置断点,也能在执行模块初始化之前停下来。 

II、在Linux 2.6.x内核中的内核模块调试方法

Linux 2.6之后的内核中,由于module-init-tools工具的更改,insmod命令不再支持-m参数,只有采取其他的方法来获取模块加载到内核的地址。通过分析ELF文件格式,我们知道程序中各段的意义如下: 

.text(代码段):用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存种的镜像。 

.data(数据段):数据段用来存放可执行文件中已初始化全局变量,也就是存放程序静态分配的变量和全局变量。 

.bss(BSS段):BSS段包含了程序中未初始化全局变量,在内存中 bss段全部置零。

  .rodata(只读段):该段保存着只读数据,在进程映象中构造不可写的段。 

通过在模块初始化函数中放置一下代码,我们可以很容易地获得模块加载到内存中的地址。 

…… int bss_var; static int hello_init(void) { printk(KERN_ALERT "Text location .text(Code Segment):%p\n",hello_init); static int data_var=0; printk(KERN_ALERT "Data Location .data(Data Segment):%p\n",&data_var); printk(KERN_ALERT "BSS Location: .bss(BSS Segment):%p\n",&bss_var); …… } Module_init(hello_init);

这里,通过在模块的初始化函数中添加一段简单的程序,使模块在加载时打印出在内核中的加载地址。.rodata段的地址可以通过执行命令readelf -e hello.ko,取得.rodata在文件中的偏移量并加上段的align值得出。  为了使读者能够更好地进行模块的调试,kgdb项目还发布了一些脚本程序能够自动探测模块的插入并自动更新gdb中模块的符号信息。这些脚本程序的工作原理与前面解释的工作过程相似,更多的信息请阅读参考资料[4]。 2.5  硬件断点  kgdb提供对硬件调试寄存器的支持。在kgdb中可以设置三种硬件断点:执行断点(Execution Breakpoint)、写断点(Write Breakpoint)、访问断点(Access Breakpoint)但不支持I/O访问的断点。 目前,kgdb对硬件断点的支持是通过宏来实现的,最多可以设置4个硬件断点,这些宏的用法如下: 

  在有些情况下,硬件断点的使用对于内核的调试是非常方便的。 

3  在VMware中搭建调试环境

kgdb调试环境需要使用两台微机分别充当development机和target机,使用VMware后我们只使用一台计算机就可以顺利完成kgdb调试环境的搭建。以windows下的环境为例,创建两台虚拟机,一台作为开发机,一台作为目标机。 3.1  虚拟机之间的串口连接  虚拟机中的串口连接可以采用两种方法。一种是指定虚拟机的串口连接到实际的COM上,例如开发机连接到COM1,目标机连接到COM2,然后把两个串口通过串口线相连接。另一种更为简便的方法是:在较高一些版本的VMware中都支持把串口映射到命名管道,把两个虚拟机的串口映射到同一个命名管道。例如,在两个虚拟机中都选定同一个命名管道 \\.\pipe\com_1,指定target机的COM口为server端,并选择"The other end is a virtual machine"属性;指定development机的COM口端为client端,同样指定COM口的"The other end is a virtual machine"属性。对于IO mode属性,在target上选中"Yield CPU on poll"复选择框,development机不选。这样,可以无需附加任何硬件,利用虚拟机就可以搭建kgdb调试环境。 即降低了使用kgdb进行调试的硬件要求,也简化了建立调试环境的过程。 

3.2  VMware的使用技巧

VMware虚拟机是比较占用资源的,尤其是象上面那样在Windows中使用两台虚拟机。因此,最好为系统配备512M以上的内存,每台虚拟机至少分配128M的内存。这样的硬件要求,对目前主流配置的PC而言并不是过高的要求。出于系统性能的考虑,在VMware中尽量使用字符界面进行调试工作。同时,Linux系统默认情况下开启了sshd服务,建议使用SecureCRT登陆到Linux进行操作,这样可以有较好的用户使用界面。 3.3  在Linux下的虚拟机中使用kgdb  对于在Linux下面使用VMware虚拟机的情况,笔者没有做过实际的探索。从原理上而言,只需要在Linux下只要创建一台虚拟机作为target机,开发机的工作可以在实际的Linux环境中进行,搭建调试环境的过程与上面所述的过程类似。由于只需要创建一台虚拟机,所以使用Linux下的虚拟机搭建kgdb调试环境对系统性能的要求较低。(vmware已经推出了Linux下的版本)还可以在development机上配合使用一些其他的调试工具,例如功能更强大的cgdb、图形界面的DDD调试器等,以方便内核的调试工作。 

4  kgdb的一些特点和不足

使用kgdb作为内核调试环境最大的不足在于对kgdb硬件环境的要求较高,必须使用两台计算机分别作为target和development机。尽管使用虚拟机的方法可以只用一台PC即能搭建调试环境,但是对系统其他方面的性能也提出了一定的要求,同时也增加了搭建调试环境时复杂程度。另外,kgdb内核的编译、配置也比较复杂,需要一定的技巧,笔者当时做的时候也是费了很多周折。当调试过程结束后时,还需要重新制作所要发布的内核。使用kgdb并不能进行全程调试,也就是说kgdb并不能用于调试系统一开始的初始化引导过程。  不过,kgdb是一个不错的内核调试工具,使用它可以进行对内核的全面调试,甚至可以调试内核的中断处理程序。如果在一些图形化的开发工具的帮助下,对内核的调试将更方便。  参考: 透过虚拟化技术体验kgdb Linux 系统内核的调试 Debugging The Linux Kernel Using Gdb

十  使用SkyEye构建Linux内核调试环境

SkyEye是一个开源软件项目(OPenSource Software),SkyEye项目的目标是在通用的Linux和Windows平台上模拟常见的嵌入式计算机系统。SkyEye实现了一个指令级的硬件模拟平台,可以模拟多种嵌入式开发板,支持多种CPU指令集。SkyEye 的核心是 GNU 的 gdb 项目,它把gdb和 ARM Simulator很好地结合在了一起。加入ARMulator 的功能之后,它就可以来仿真嵌入式开发板,在它上面不仅可以调试硬件驱动,还可以调试操作系统。Skyeye项目目前已经在嵌入式系统开发领域得到了很大的推广。 

1  SkyEye的安装和μcLinux内核编译

1.1  SkyEye的安装  SkyEye的安装不是本文要介绍的重点,目前已经有大量的资料对此进行了介绍。有关SkyEye的安装与使用的内容请查阅参考资料[11]。由于skyeye面目主要用于嵌入式系统领域,所以在skyeye上经常使用的是μcLinux系统,当然使用Linux作为skyeye上运行的系统也是可以的。由于介绍μcLinux 2.6在skyeye上编译的相关资料并不多,所以下面进行详细介绍。 1.2  μcLinux 2.6.x的编译  要在SkyEye中调试操作系统内核,首先必须使被调试内核能在SkyEye所模拟的开发板上正确运行。因此,正确编译待调试操作系统内核并配置SkyEye是进行内核调试的第一步。下面我们以SkyEye模拟基于Atmel AT91X40的开发板,并运行μcLinux 2.6为例介绍SkyEye的具体调试方法。 I、安装交叉编译环境  先安装交叉编译器。尽管在一些资料中说明使用工具链arm-elf-tools-20040427.sh ,但是由于arm-elf-xxx与arm-linux-xxx对宏及链接处理的不同,经验证明使用arm-elf-xxx工具链在链接vmlinux的最后阶段将会出错。所以这里我们使用的交叉编译工具链是:arm-uclinux-tools-base-gcc3.4.0-20040713.sh,关于该交叉编译工具链的下载地址请参见[6]。注意以下步骤最好用root用户来执行。 

[root@lisl tmp]#chmod +x arm-uclinux-tools-base-gcc3.4.0-20040713.sh [root@lisl tmp]#./arm-uclinux-tools-base-gcc3.4.0-20040713.sh

安装交叉编译工具链之后,请确保工具链安装路径存在于系统PATH变量中。 

II、制作μcLinux内核  得到μcLinux发布包的一个最容易的方法是直接访问uClinux.org站点[7]。该站点发布的内核版本可能不是最新的,但你能找到一个最新的μcLinux补丁以及找一个对应的Linux内核版本来制作一个最新的μcLinux内核。这里,将使用这种方法来制作最新的μcLinux内核。目前(笔者记录编写此文章时),所能得到的发布包的最新版本是uClinux-dist.20041215.tar.gz。  下载uClinux-dist.20041215.tar.gz,文件的下载地址请参见[7]。  下载linux-2.6.9-hsc0.patch.gz,文件的下载地址请参见[8]。  下载linux-2.6.9.tar.bz2,文件的下载地址请参见[9]。  现在我们得到了整个的linux-2.6.9源代码,以及所需的内核补丁。请准备一个有2GB空间的目录里来完成以下制作μcLinux内核的过程。 

[root@lisl tmp]# tar -jxvf uClinux-dist-20041215.tar.bz2 [root@lisl uClinux-dist]# tar -jxvf linux-2.6.9.tar.bz2 [root@lisl uClinux-dist]# gzip -dc linux-2.6.9-hsc0.patch.gz | patch -p0

或者使用:

[root@lisl uClinux-dist]# gunzip linux-2.6.9-hsc0.patch.gz [root@lisl uClinux-dist]patch -p0 < linux-2.6.9-hsc0.patch

执行以上过程后,将在linux-2.6.9/arch目录下生成一个补丁目录-armnommu。删除原来μcLinux目录里的linux-2.6.x(即那个linux-2.6.9-uc0),并将我们打好补丁的Linux内核目录更名为linux-2.6.x。 

[root@lisl uClinux-dist]# rm -rf linux-2.6.x/ [root@lisl uClinux-dist]# mv linux-2.6.9 linux-2.6.x

III、配置和编译μcLinux内核  因为只是出于调试μcLinux内核的目的,这里没有生成uClibc库文件及romfs.img文件。在发布μcLinux时,已经预置了某些常用嵌入式开发板的配置文件,因此这里直接使用这些配置文件,过程如下: 

[root@lisl uClinux-dist]# cd linux-2.6.x [root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- atmel_deconfig

atmel_deconfig文件是μcLinux发布时提供的一个配置文件,存放于目录linux-2.6.x /arch/armnommu/configs/中。 

[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- oldconfig

下面编译配置好的内核: 

[root@lisl linux-2.6.x]# make ARCH=armnommu CROSS_COMPILE=arm-uclinux- v=1

一般情况下,编译将顺利结束并在Linux-2.6.x/目录下生成未经压缩的μcLinux内核文件vmlinux。需要注意的是为了调试μcLinux内核,需要打开内核编译的调试选项-g,使编译后的内核带有调试信息。打开编译选项的方法可以选择:  "Kernel debugging->Compile the kernel with debug info"后将自动打开调试选项。也可以直接修改linux-2.6.x目录下的Makefile文件,为其打开调试开关。方法如下:。 

CFLAGS += -g

最容易出现的问题是找不到arm-uclinux-gcc命令的错误,主要原因是PATH变量中没有 包含arm-uclinux-gcc命令所在目录。在arm-linux-gcc的缺省安装情况下,它的安装目录是/root/bin/arm-linux-tool/,使用以下命令将路径加到PATH环境变量中。 

Export PATH=$PATH:/root/bin/arm-linux-tool/bin

IV、根文件系统的制作  Linux内核在启动的时的最后操作之一是加载根文件系统。根文件系统中存放了嵌入式 系统使用的所有应用程序、库文件及其他一些需要用到的服务。出于文章篇幅的考虑,这里不打算介绍根文件系统的制作方法,读者可以查阅一些其他的相关资料。值得注意的是,由配置文件skyeye.conf指定了装载到内核中的根文件系统。 

2  使用SkyEye调试

编译完μcLinux内核后,就可以在SkyEye中调试该ELF执行文件格式的内核了。前面已经说过利用SkyEye调试内核与使用gdb调试运用程序的方法相同。  需要提醒读者的是,SkyEye的配置文件-skyeye.conf记录了模拟的硬件配置和模拟执行行为。该配置文件是SkyEye系统中一个及其重要的文件,很多错误和异常情况的发生都和该文件有关。在安装配置SkyEye出错时,请首先检查该配置文件然后再进行其他的工作。此时,所有的准备工作已经完成,就可以进行内核的调试工作了。 

3  使用SkyEye调试内核的特点和不足

在SkyEye中可以进行对Linux系统内核的全程调试。由于SkyEye目前主要支持基于ARM内核的CPU,因此一般而言需要使用交叉编译工具编译待调试的Linux系统内核。另外,制作SkyEye中使用的内核编译、配置过程比较复杂、繁琐。不过,当调试过程结束后无需重新制作所要发布的内核。  SkyEye只是对系统硬件进行了一定程度上的模拟,所以在SkyEye与真实硬件环境相比较而言还是有一定的差距,这对一些与硬件紧密相关的调试可能会有一定的影响,例如驱动程序的调试。不过对于大部分软件的调试,SkyEye已经提供了精度足够的模拟了。  SkyEye的下一个目标是和eclipse结合,有了图形界面,能为调试和查看源码提供一些方便。  参考: Linux 系统内核的调试   

十一  KDB

Linux 内核调试器(KDB)允许您调试 Linux 内核。这个恰如其名的工具实质上是内核代码的补丁,它允许高手访问内核内存和数据结构。KDB 的主要优点之一就是它不需要用另一台机器进行调试:您可以调试正在运行的内核。  设置一台用于 KDB 的机器需要花费一些工作,因为需要给内核打补丁并进行重新编译。KDB 的用户应当熟悉 Linux 内核的编译(在一定程度上还要熟悉内核内部机理)。  在本文中,我们将从有关下载 KDB 补丁、打补丁、(重新)编译内核以及启动 KDB 方面的信息着手。然后我们将了解 KDB 命令并研究一些较常用的命令。最后,我们将研究一下有关设置和显示选项方面的一些详细信息。 

1  入门

KDB 项目是由 Silicon Graphics 维护的,您需要从它的 FTP 站点下载与内核版本有关的补丁。(在编写本文时)可用的最新 KDB 版本是 4.2。您将需要下载并应用两个补丁。一个是“公共的”补丁,包含了对通用内核代码的更改,另一个是特定于体系结构的补丁。补丁可作为 bz2 文件获取。例如,在运行 2.4.20 内核的 x86 机器上,您会需要 kdb-v4.2-2.4.20-common-1.bz2 和 kdb-v4.2-2.4.20-i386-1.bz2。  这里所提供的所有示例都是针对 i386 体系结构和 2.4.20 内核的。您将需要根据您的机器和内核版本进行适当的更改。您还需要拥有 root 许可权以执行这些操作。  将文件复制到 /usr/src/linux 目录中并从用 bzip2 压缩的文件解压缩补丁文件:

#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2 #bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2

您将获得 kdb-v4.2-2.4.20-common-1 和 kdb-v4.2-2.4-i386-1 文件。  现在,应用这些补丁:

#patch -p1 /proc/sys/kernel/kdb

在引导期间还可以将另一个标志传递给内核。 kdb=early 标志将导致在引导过程的初始阶段就把控制权传递给 KDB。如果您需要在引导过程初始阶段进行调试,那么这将有所帮助。  调用 KDB 的方式有很多。如果 KDB 处于打开状态,那么只要内核中有紧急情况就自动调用它。按下键盘上的 PAUSE 键将手工调用 KDB。调用 KDB 的另一种方式是通过串行控制台。当然,要做到这一点,需要设置串行控制台并且需要一个从串行控制台进行读取的程序。按键序列 Ctrl-A 将从串行控制台调用 KDB。 

4  KDB 命令

KDB 是一个功能非常强大的工具,它允许进行几个操作,比如内存和寄存器修改、应用断点和堆栈跟踪。根据这些,可以将 KDB 命令分成几个类别。下面是有关每一类中最常用命令的详细信息。 4.1  内存显示和修改  这一类别中最常用的命令是 md 、 mdr 、 mm 和 mmW 。  md 命令以一个地址/符号和行计数为参数,显示从该地址开始的 line-count 行的内存。如果没有指定 line-count ,那么就使用环境变量所指定的缺省值。如果没有指定地址,那么 md 就从上一次打印的地址继续。地址打印在开头,字符转换打印在结尾。  mdr 命令带有地址/符号以及字节计数,显示从指定的地址开始的 byte-count 字节数的初始内存内容。它本质上和 md 一样,但是它不显示起始地址并且不在结尾显示字符转换。 mdr 命令较少使用。  mm 命令修改内存内容。它以地址/符号和新内容作为参数,用 new-contents 替换地址处的内容。  mmW 命令更改从地址开始的 W 个字节。请注意, mm 更改一个机器字。  示例  显示从 0xc000000 开始的 15 行内存:

[0]kdb> md 0xc000000 15

将内存位置为 0xc000000 上的内容更改为 0x10:

[0]kdb> mm 0xc000000 0x10

4.2  寄存器显示和修改  这一类别中的命令有 rd 、 rm 和 ef 。  rd 命令(不带任何参数)显示处理器寄存器的内容。它可以有选择地带三个参数。如果传递了 c 参数,则 rd 显示处理器的控制寄存器;如果带有 d 参数,那么它就显示调试寄存器;如果带有 u 参数,则显示上一次进入内核的当前任务的寄存器组。  rm 命令修改寄存器的内容。它以寄存器名称和 new-contents 作为参数,用 new-contents 修改寄存器。寄存器名称与特定的体系结构有关。目前,不能修改控制寄存器。  ef 命令以一个地址作为参数,它显示指定地址处的异常帧。  示例  显示通用寄存器组:

[0]kdb> rd [0]kdb> rm %ebx 0x25

4.3  断点  常用的断点命令有 bp 、 bc 、 bd 、 be 和 bl 。  bp 命令以一个地址/符号作为参数,它在地址处应用断点。当遇到该断点时则停止执行并将控制权交予 KDB。该命令有几个有用的变体。 bpa 命令对 SMP 系统中的所有处理器应用断点。 bph 命令强制在支持硬件寄存器的系统上使用它。 bpha 命令类似于 bpa 命令,差别在于它强制使用硬件寄存器。 bd 命令禁用特殊断点。它接收断点号作为参数。该命令不是从断点表中除去断点,而只是禁用它。断点号从 0 开始,根据可用性顺序分配给断点。 be 命令启用断点。该命令的参数也是断点号。 bl 命令列出当前的断点集。它包含了启用的和禁用的断点。 bc 命令从断点表中除去断点。它以具体的断点号或 * 作为参数,在后一种情况下它将除去所有断点。  示例  对函数 sys_write() 设置断点:

[0]kdb> bp sys_write

列出断点表中的所有断点:

[0]kdb> bl

清除断点号 1:

[0]kdb> bc 1

4.4  堆栈跟踪  主要的堆栈跟踪命令有 bt 、 btp 、 btc 和 bta 。  bt 命令设法提供有关当前线程的堆栈的信息。它可以有选择地将堆栈帧地址作为参数。如果没有提供地址,那么它采用当前寄存器来回溯堆栈。否则,它假定所提供的地址是有效的堆栈帧起始地址并设法进行回溯。如果内核编译期间设置了CONFIG_FRAME_POINTER 选项,那么就用帧指针寄存器来维护堆栈,从而就可以正确地执行堆栈回溯。如果没有设置CONFIG_FRAME_POINTER ,那么 bt 命令可能会产生错误的结果。  btp 命令将进程标识作为参数,并对这个特定进程进行堆栈回溯。  btc 命令对每个活动 CPU 上正在运行的进程执行堆栈回溯。它从第一个活动 CPU 开始执行 bt ,然后切换到下一个活动 CPU,以此类推。  bta 命令对处于某种特定状态的所有进程执行回溯。若不带任何参数,它就对所有进程执行回溯。可以有选择地将各种参数传递给该命令。将根据参数处理处于特定状态的进程。选项以及相应的状态如下: 

D:不可中断状态R:正运行S:可中断休眠T:已跟踪或已停止Z:僵死U:不可运行

这类命令中的每一个都会打印出一大堆信息。

示例  跟踪当前活动线程的堆栈:

[0]kdb> bt

跟踪标识为 575 的进程的堆栈:

[0]kdb> btp 575

4.5  其它命令  下面是在内核调试过程中非常有用的其它几个 KDB 命令。  id 命令以一个地址/符号作为参数,它对从该地址开始的指令进行反汇编。环境变量 IDCOUNT 确定要显示多少行输出。  ss 命令单步执行指令然后将控制返回给 KDB。该指令的一个变体是 ssb ,它执行从当前指令指针地址开始的指令(在屏幕上打印指令),直到它遇到将引起分支转移的指令为止。分支转移指令的典型示例有 call 、 return 和 jump 。  go 命令让系统继续正常执行。一直执行到遇到断点为止(如果已应用了一个断点的话)。  reboot 命令立刻重新引导系统。它并没有彻底关闭系统,因此结果是不可预测的。  ll 命令以地址、偏移量和另一个 KDB 命令作为参数。它对链表中的每个元素反复执行作为参数的这个命令。所执行的命令以列表中当前元素的地址作为参数。  示例  反汇编从例程 schedule 开始的指令。所显示的行数取决于环境变量 IDCOUNT :

[0]kdb> id schedule

执行指令直到它遇到分支转移条件(在本例中为指令 jne )为止:

[0]kdb> ssb 0xc0105355 default_idle+0x25: cli 0xc0105356 default_idle+0x26: mov 0x14(%edx),%eax 0xc0105359 default_idle+0x29: test %eax, %eax 0xc010535b default_idle+0x2b: jne 0xc0105361 default_idle+0x31 5  技巧和诀窍

调试一个问题涉及到:使用调试器(或任何其它工具)找到问题的根源以及使用源代码来跟踪导致问题的根源。单单使用源代码来确定问题是极其困难的,只有老练的内核黑客才有可能做得到。相反,大多数的新手往往要过多地依靠调试器来修正错误。这种方法可能会产生不正确的问题解决方案。我们担心的是这种方法只会修正表面症状而不能解决真正的问题。此类错误的典型示例是添加错误处理代码以处理 NULL 指针或错误的引用,却没有查出无效引用的真正原因。  结合研究代码和使用调试工具这两种方法是识别和修正问题的最佳方案。  调试器的主要用途是找到错误的位置、确认症状(在某些情况下还有起因)、确定变量的值,以及确定程序是如何出现这种情况的(即,建立调用堆栈)。有经验的黑客会知道对于某种特定的问题应使用哪一个调试器,并且能迅速地根据调试获取必要的信息,然后继续分析代码以识别起因。  因此,这里为您介绍了一些技巧,以便您能使用 KDB 快速地取得上述结果。当然,要记住,调试的速度和精确度来自经验、实践和良好的系统知识(硬件和内核内部机理等)。 5.1  技巧 #1  在 KDB 中,在提示处输入地址将返回与之最为匹配的符号。这在堆栈分析以及确定全局数据的地址/值和函数地址方面极其有用。同样,输入符号名则返回其虚拟地址。  示例  表明函数 sys_read 从地址 0xc013db4c 开始:

[0]kdb> 0xc013db4c 0xc013db4c = 0xc013db4c (sys_read)

同样,表明 sys_write 位于地址 0xc013dcc8:

[0]kdb> sys_write sys_write = 0xc013dcc8 (sys_write)

这些有助于在分析堆栈时找到全局数据和函数地址。 5.2  技巧 #2  在编译带 KDB 的内核时,只要 CONFIG_FRAME_POINTER 选项出现就使用该选项。为此,需要在配置内核时选择“Kernel hacking”部分下面的“Compile the kernel with frame pointers”选项。这确保了帧指针寄存器将被用作帧指针,从而产生正确的回溯。实际上,您可以手工转储帧指针寄存器的内容并跟踪整个堆栈。例如,在 i386 机器上,%ebp 寄存器可以用来回溯整个堆栈。  例如,在函数 rmqueue() 上执行第一个指令后,堆栈看上去类似于下面这样:

[0]kdb> md %ebp 0xc74c9f38 c74c9f60 c0136c40 000001f0 00000000 0xc74c9f48 08053328 c0425238 c04253a8 00000000 0xc74c9f58 000001f0 00000246 c74c9f6c c0136a25 0xc74c9f68 c74c8000 c74c9f74 c0136d6d c74c9fbc 0xc74c9f78 c014fe45 c74c8000 00000000 08053328 [0]kdb> 0xc0136c40 0xc0136c40 = 0xc0136c40 (__alloc_pages +0x44) [0]kdb> 0xc0136a25 0xc0136a25 = 0xc0136a25 (_alloc_pages +0x19) [0]kdb> 0xc0136d6d 0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)

我们可以看到 rmqueue() 被 __alloc_pages 调用,后者接下来又被 _alloc_pages 调用,以此类推。  每一帧的第一个双字(double word)指向下一帧,这后面紧跟着调用函数的地址。因此,跟踪堆栈就变成一件轻松的工作了。 5.3  技巧 #3  go 命令可以有选择地以一个地址作为参数。如果您想在某个特定地址处继续执行,则可以提供该地址作为参数。另一个办法是使用rm 命令修改指令指针寄存器,然后只要输入 go 。如果您想跳过似乎会引起问题的某个特定指令或一组指令,这就会很有用。但是,请注意,该指令使用不慎会造成严重的问题,系统可能会严重崩溃。 5.4  技巧 #4  您可以利用一个名为 defcmd 的有用命令来定义自己的命令集。例如,每当遇到断点时,您可能希望能同时检查某个特殊变量、检查某些寄存器的内容并转储堆栈。通常,您必须要输入一系列命令,以便能同时执行所有这些工作。 defcmd 允许您定义自己的命令,该命令可以包含一个或多个预定义的 KDB 命令。然后只需要用一个命令就可以完成所有这三项工作。其语法如下:

[0]kdb> defcmd name "usage" "help" [0]kdb> [defcmd] type the commands here [0]kdb> [defcmd] endefcmd

例如,可以定义一个(简单的)新命令 hari ,它显示从地址 0xc000000 开始的一行内存、显示寄存器的内容并转储堆栈:

[0]kdb> defcmd hari "" "no arguments needed" [0]kdb> [defcmd] md 0xc000000 1 [0]kdb> [defcmd] rd [0]kdb> [defcmd] md %ebp 1 [0]kdb> [defcmd] endefcmd

该命令的输出会是:

[0]kdb> hari [hari]kdb> md 0xc000000 1 0xc000000 00000001 f000e816 f000e2c3 f000e816 [hari]kdb> rd eax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000 .... ... [hari]kdb> md %ebp 1 0xc0467fbc c0467fd0 c01053d2 00000002 000a0200 [0]kdb>

5.5  技巧 #5  可以使用 bph 和 bpha 命令(假如体系结构支持使用硬件寄存器)来应用读写断点。这意味着每当从某个特定地址读取数据或将数据写入该地址时,我们都可以对此进行控制。当调试数据/内存毁坏问题时这可能会极其方便,在这种情况中您可以用它来识别毁坏的代码/进程。  示例  每当将四个字节写入地址 0xc0204060 时就进入内核调试器:

[0]kdb> bph 0xc0204060 dataw 4

在读取从 0xc000000 开始的至少两个字节的数据时进入内核调试器:

[0]kdb> bph 0xc000000 datar 2 6 结束语

对于执行内核调试,KDB 是一个方便的且功能强大的工具。它提供了各种选项,并且使我们能够分析内存内容和数据结构。最妙的是,它不需要用另一台机器来执行调试。  参考: Linux 内核调试器内幕 KDB入门指南   

十二  Kprobes

Kprobes 是 Linux 中的一个简单的轻量级装置,让您可以将断点插入到正在运行的内核之中。 Kprobes 提供了一个强行进入任何内核例程并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以 轻松地收集处理器寄存器和全局数据结构等调试信息。开发者甚至可以使用 Kprobes 来修改 寄存器值和全局数据结构的值。  为完成这一任务,Kprobes 向运行的内核中给定地址写入断点指令,插入一个探测器。 执行被探测的指令会导致断点错误。Kprobes 钩住(hook in)断点处理器并收集调试信息。Kprobes 甚至可以单步执行被探测的指令。 

1  安装

要安装 Kprobes,需要从 Kprobes 主页下载最新的补丁。 打包的文件名称类似于 kprobes-2.6.8-rc1.tar.gz。解开补丁并将其安装到 Linux 内核: 

$tar -xvzf kprobes-2.6.8-rc1.tar.gz $cd /usr/src/linux-2.6.8-rc1 $patch -p1 < ../kprobes-2.6.8-rc1-base.patch

Kprobes 利用了 SysRq 键,这个 DOS 时代的产物在 Linux 中有了新的用武之地。您可以在 Scroll Lock键左边找到 SysRq 键;它通常标识为 Print Screen。要为 Kprobes 启用 SysRq 键,需要安装 kprobes-2.6.8-rc1-sysrq.patch 补丁:

$patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch

使用 make xconfig/ make menuconfig/ make oldconfig 配置内核,并 启用 CONFIG_KPROBES 和 CONFIG_MAGIC_SYSRQ标记。 编译并引导到新内核。您现在就已经准备就绪,可以插入 printk 并通过编写简单的 Kprobes 模块来动态而且无干扰地 收集调试信息。 

2  编写 Kprobes 模块

对于每一个探测器,您都要分配一个结构体 struct kprobe kp; (参考 include/linux/kprobes.h 以获得关于此数据结构的详细信息)。  清单 9. 定义 pre、post 和 fault 处理器

/* pre_handler: this is called just before the probed instruction is * executed. */ int handler_pre(struct kprobe *p, struct pt_regs *regs) { printk("pre_handler: p->addr=0x%p, eflags=0x%lx\n",p->addr, regs->eflags); return 0; } /* post_handler: this is called after the probed instruction is executed * (provided no exception is generated). */ void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) { printk("post_handler: p->addr=0x%p, eflags=0x%lx \n", p->addr, regs->eflags); } /* fault_handler: this is called if an exception is generated for any * instruction within the fault-handler, or when Kprobes * single-steps the probed instruction. */ int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) { printk("fault_handler:p->addr=0x%p, eflags=0x%lx\n", p->addr, regs->eflags); return 0; }

2.1  获得内核例程的地址  在注册过程中,您还需要指定插入探测器的内核例程的地址。使用这些方法中的任意一个来获得内核例程 的地址: 

从 System.map 文件直接得到地址。例如,要得到 do_fork 的地址,可以在命令行执行 $grep do_fork /usr/src/linux/System.map 。使用 nm 命令。$nm vmlinuz |grep do_fork从 /proc/kallsyms 文件获得地址。$cat /proc/kallsyms |grep do_fork使用 kallsyms_lookup_name() 例程。这个例程是在 kernel/kallsyms.c 文件中定义的,要使用它,必须启用 CONFIG_KALLSYMS 编译内核。kallsyms_lookup_name() 接受一个字符串格式内核例程名, 返回那个内核例程的地址。例如:kallsyms_lookup_name("do_fork");

然后在 init_moudle 中注册您的探测器:  清单 10. 注册一个探测器 

/* specify pre_handler address */ kp.pre_handler=handler_pre; /* specify post_handler address */ kp.post_handler=handler_post; /* specify fault_handler address */ kp.fault_handler=handler_fault; /* specify the address/offset where you want to insert probe. * You can get the address using one of the methods described above. */ kp.addr = (kprobe_opcode_t *) kallsyms_lookup_name("do_fork"); /* check if the kallsyms_lookup_name() returned the correct value. */ if (kp.add == NULL) { printk("kallsyms_lookup_name could not find address for the specified symbol name\n"); return 1; } /* or specify address directly. * $grep "do_fork" /usr/src/linux/System.map * or * $cat /proc/kallsyms |grep do_fork * or * $nm vmlinuz |grep do_fork */ kp.addr = (kprobe_opcode_t *) 0xc01441d0; /* All set to register with Kprobes */ register_kprobe(&kp);

一旦注册了探测器,运行任何 shell 命令都会导致一个对 do_fork 的调用,您将可以在控制台上或者运行 dmesg 命令来查看您的 printk。做完后要记得注销探测器:  unregister_kprobe(&kp);  下面的输出显示了 kprobe 的地址以及 eflags 寄存器的内容: 

$tail -5 /var/log/messages Jun 14 18:21:18 llm05 kernel: pre_handler: p->addr=0xc01441d0, eflags=0x202 Jun 14 18:21:18 llm05 kernel: post_handler: p->addr=0xc01441d0, eflags=0x196

2.2  获得偏移量  您可以在例程的开头或者函数中的任意偏移位置插入 printk(偏移量必须在指令范围之内)。 下面的代码示例展示了如何来计算偏移量。首先,从对象文件中反汇编机器指令,并将它们 保存为一个文件: 

$objdump -D /usr/src/linux/kernel/fork.o > fork.dis

其结果是:  清单 11. 反汇编的 fork

000022b0 : 22b0: 55 push %ebp 22b1: 89 e5 mov %esp,%ebp 22b3: 57 push %edi 22b4: 89 c7 mov %eax,%edi 22b6: 56 push %esi 22b7: 89 d6 mov %edx,%esi 22b9: 53 push %ebx 22ba: 83 ec 38 sub $0x38,%esp 22bd: c7 45 d0 00 00 00 00 movl $0x0,0xffffffd0(%ebp) 22c4: 89 cb mov %ecx,%ebx 22c6: 89 44 24 04 mov %eax,0x4(%esp) 22ca: c7 04 24 0a 00 00 00 movl $0xa,(%esp) 22d1: e8 fc ff ff ff call 22d2 22d6: b8 00 e0 ff ff mov $0xffffe000,%eax 22db: 21 e0 and %esp,%eax 22dd: 8b 00 mov (%eax),%eax

要在偏移位置 0x22c4 插入探测器,先要得到与例程的开始处相对的偏移量 0x22c4 - 0x22b0 = 0x14 ,然后将这个偏移量添加到 do_fork 的地址 0xc01441d0 + 0x14 。(运行 $cat /proc/kallsyms | grep do_fork 命令以获得 do_fork 的地址。)  您还可以将 do_fork 的相对偏移量 0x22c4 - 0x22b0 = 0x14 添加到 kallsyms_lookup_name("do_fork"); 的输入,即:0x14 + kallsyms_lookup_name("do_fork"); 2.3  转储内核数据结构  现在,让我们使用修改过的用来转储数据结构的 Kprobe post_handler 来转储运行在系统上的所有作业的一些组成部分:  清单 12. 用来转储数据结构的修改过的 Kprope post_handler

void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) { struct task_struct *task; read_lock(&tasklist_lock); for_each_process(task) { printk("pid =%x task-info_ptr=%lx\n", task->pid, task->thread_info); printk("thread-info element status=%lx,flags=%lx,cpu=%lx\n", task->thread_info->status, task->thread_info->flags, task->thread_info->cpu); } read_unlock(&tasklist_lock); }

这个模块应该插入到 do_fork 的偏移位置。  清单 13. pid 1508 和 1509 的结构体 thread_info 的输出

$tail -10 /var/log/messages Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=0, cpu=1 Jun 22 18:14:25 llm05 kernel: pid =5e4 task-info_ptr=f5948000 Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=8, cpu=0 Jun 22 18:14:25 llm05 kernel: pid =5e5 task-info_ptr=f5eca000

2.4  启用奇妙的 SysRq 键  为了支持 SysRq 键,我们已经进行了编译。这样来启用它: 

$echo 1 > /proc/sys/kernel/sysrq

现在,您可以使用 Alt+SysRq+W 在控制台上或者到 /var/log/messages 中去查看所有插入的内核探测器。  清单 14. /var/log/messages 显示出在 do_fork 插入了一个 Kprobe

Jun 23 10:24:48 linux-udp4749545uds kernel: SysRq : Show kprobes Jun 23 10:24:48 linux-udp4749545uds kernel: Jun 23 10:24:48 linux-udp4749545uds kernel: [] do_fork+0x0/0x1de 3  使用 Kprobes 更好地进行调试

由于探测器事件处理器是作为系统断点中断处理器的扩展来运行,所以它们很少或者根本不依赖于系统 工具 —— 这样可以被植入到大部分不友好的环境中(从中断时间和任务时间到禁用的上下文间切换和支持 SMP 的代码路径)—— 都不会对系统性能带来负面影响。  使用 Kprobes 的好处有很多。不需要重新编译和重新引导内核就可以插入 printk。为了进行调试可以记录 处理器寄存器的日志,甚至进行修改 —— 不会干扰系统。类似地,同样可以无干扰地记录 Linux 内核数据结构的日志,甚至 进行修改。您甚至可以使用 Kprobes 调试 SMP 系统上的竞态条件 —— 避免了您自己重新编译和重新引导的所有 麻烦。您将发现内核调试比以往更为快速和简单。  参考: 使用 Kprobes 调试内核  本文参考:  1  Linux内核设计与实现   P243 第十八章 调试  3   linux内核调试指南



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3